feat(focus): fail-close send-safety gate on approve (C-SEND-1)#659
Conversation
The dry-run preview (WouldSendPreview + resolveProvenance) already exists and is correct - so C-SEND-1 is not a preview build. The real gap was a safety hole: the decide route queued a real outreach send gated ONLY by the OUTREACH_CONSUMER_ENABLED env var, with zero awareness of card provenance or PILOT_LOCK. Approving a sample/shadow card could queue a real send if that one flag flipped - unsafe-by-config, not safe-by-contract. This adds a fail-close send-safety contract (resolveSendDecision): an approved outreach is queued ONLY when the item is genuinely live AND the tenant is pilot-unlocked AND the consumer is enabled. Any missing condition HOLDS the send while still recording the approval (outcome_receipts). Reuses the pure provenance guards + isPilotLockEnabled; no new lock/provenance definition. proof_events is never written by the console (Pulse-owned) - locked by an invariant test. Tests: 8-combo truth table for resolveSendDecision; decide-route cases for live-queues / sample-held / pilot-held / consumer-held (+ approval always recorded); proof=0 invariant (approve never writes proof_events; empty ledger = 0/0). Makes the console consistent with Pulse's PILOT_LOCK fail-close boundary (defense in depth) so the eventual First-Light send is safe-by-contract. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
🛡️ Cascade Quality Score: 100/100
Threshold: 85/100 | Result: PASS ✅ |
…urce-guard) outreach-consumer-safety.test.ts asserts every send-capable route references the consumer-pause safety by that literal phrase. The C-SEND-1 reword dropped it from the decide route; restore it (with a comment) so the guard passes. Behavior unchanged - still fail-close held. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ent) Completes the C-SEND-1 arc: the decide route now returns send_blocked_reason when the fail-close gate holds an approved send, but the Deck ignored it - an approved-but-held card just showed 'Approved', implying it was delivered. While PILOT_LOCK holds every send (First Light), the owner would think approvals went out. Now handleDecision (single + bulk) captures the held reason and the card shows an honest 'Prepared, not sent' chip with a plain reason on hover (pilot lock on / preview only / delivery paused). Neutral stone chip + amber-300 accent (held is informational, not interactive/error). No new component - extends the existing card status row next to the 'Queued' chip. No fabricated proof; nothing sent. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Extended: added the owner-visible held-state that completes C-SEND-1. The gate now returns |
…p jargon (rank-2)
Pre-First-Light gate rank-2: the held-state reason lived only in a title= tooltip
(invisible on Elaine's touch device), used jargon ('Pilot lock on, nothing sends
yet'), and gave no 'what happens next'. Now: (1) plain reassuring labels that say
the approval is SAVED + when it sends ('Saved. It sends once testing wraps up;
nothing has gone out yet.'), no 'pilot lock' jargon; (2) rendered as a VISIBLE
inline line under the card (works on touch), in addition to the glanceable
'Prepared, not sent' chip. Closes the C-SEND-1 arc.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Extended (rank-2 from the pre-First-Light gate): the held-state reason was tooltip-only (invisible on touch), jargon, and gave no next-step. Now plain-English reassuring labels ("Saved. It sends once testing wraps up; nothing has gone out yet" - no "pilot lock" jargon) rendered as a visible inline line under the card (works on Elaine's touch device), alongside the glanceable "Prepared, not sent" chip. This closes the C-SEND-1 arc + the last Claude gate item. |
250b0ab
into
main
C-SEND-1 — fail-close send-safety gate on approve
Designed by an 8-agent understand→design→adversarial-verify workflow before any code. The key finding reframed the task:
The dry-run preview already exists and is correct (
WouldSendPreview+resolveProvenance+TrustStrip+silent-pilot.test.tsx). So C-SEND-1 is not a preview build — do not rebuild those.The real gap = a safety hole
app/api/v1/focus/decide/route.tsqueued a real outreach send gated only by theOUTREACH_CONSUMER_ENABLEDenv var — with zero awareness of card provenance or PILOT_LOCK. Approving a sample/shadow card could queue a real send if that one flag flipped. That is unsafe-by-config, not safe-by-contract — and it means the eventual First-Light send is not yet a tested one-click.The fix (smallest honest change, reuses every existing primitive)
lib/outreach/safety.ts— new pureresolveSendDecision({ sends, pilotLocked, consumerEnabled }). An external send is queued ONLY when live AND pilot-unlocked AND consumer-enabled; any missing condition holds it. Precedencepilot_lock → not_live_provenance → consumer_paused. It can never become true by a single env flip.sendsby reusing the pure provenance guards (isDataProvenance/isSideEffectMode, mirroringresolveProvenance's live-rule) read defensively off the row (recovery_itemshas no provenance column today → absent → not live → held = fail-close), plusisPilotLockEnabled(). The approval (outcome_receipts) is always recorded even when the send is held. Adds structuredwould_send+send_blocked_reasonto the response (assert on fields, not prose).outcome_receiptsalready IS the approval log;proof_eventsstays Pulse-owned.Tests
lib/outreach/safety.test.ts— full 8-combo truth table (exactly one allowed; pilot lock always dominates).__tests__/focus-decide-route.test.ts— live-queues / sample-held (not_live_provenance) / pilot-held (pilot_lock) / consumer-held (consumer_paused); approval receipt recorded in every held case.__tests__/csend-proof-invariant.test.ts— approve never writesproof_events; empty ledger =0proof_events /0wins. Codifies the proof=0 honesty invariant.Honesty / safety
Makes the console consistent with Pulse's
PILOT_LOCKfail-close boundary (defense in depth). Atproof_events=0with HRC pilot-locked, nothing queues — by contract, not by luck. Built off freshorigin/mainin an isolated worktree; the parked console dev branch was untouched. node_modules absent in the worktree → console CI (Build+Lint+Test, Quality Gate) is the test gate.Generated with Claude Code by RelayLaunch